// Copyright (c) 2009 - Quildreen <quildreen@gmail.com>                     
//                                                                          
//  Permission is hereby granted, free of charge, to any person              
//  obtaining a copy of this software and associated documentation files     
//  (the "Software"), to deal in the Software without restriction,           
//  including without limitation the rights to use, copy, modify, merge,     
//  publish, distribute, sublicense, and/or sell copies of the Software,     
//  and to permit persons to whom the Software is furnished to do so,        
//  subject to the following conditions:                                     
//                                                                           
//  The above copyright notice and this permission notice shall be           
//  included in all copies or substantial portions of the Software.          
//                                                                           
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,          
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF       
//  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                    
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE   
//  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION   
//  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION    
//  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 

/**
 * lily.graphics.dom.anim
 * 
 * Implements basic animation objects
 * 
 * Requires: `graphics.dom.core`
 **/
lily.require("graphics.dom.core");
lily.onLoad(function() {
    
    lily.defineModule("graphics.dom.anim");


    /**
     * class lily.graphics.dom.anim.Strip
     * 
     * Implements a strip animation. This uses a large strip animation
     * image (one that contains all the frames of the animation) and
     * alternates the visible frame by changing the image offset.
     *
     * The object can either use the internal object timebase or the
     * application time base. The former allows you to update the
     * object only when needed (the object is visible on the screen);
     *
     * Basic surface manipulations are inherited from the DOM Surface
     * object.
     **/
    lily.graphics.dom.anim.Strip = Class.create(
    {
        /**
         * new lily.graphics.dom.anim.Strip(image, frame_size, grid_size[, speed = 1][, start = 0][, count = 0][, options])
         * - image (String | [[lily.graphics.dom.Surface]]): the image for this
         *   animation.
         * - frame_size (Array): size of the frames in the animation `[width,
         *   height]`
         * - grid_size (Array): number of columns and rows of frames in the
         *   given image, as a `[cols, rows]` array.
         * - speed (Number): animation speed in frames per second.
         * - start (Number): starting frame, using a 0-based index.
         * - count (Number): number of frames in the animation.
         * - options (Object): additional options to the animation.
         * 
         * If `count` is not given, the animation will use `cols * rows`.
         **/
        initialize: function(image, frame_size, grid_size, speed, start, count,
                             options)
        {
            if (Object.isString(image)) {
                image = new lily.graphics.dom.Surface(image, frame_size[0],
                                                      frame_size[1]);
            } else {
                image.setSize(frame_size[0], frame_size[1]);
            }

            start || (start = 0);
            count || (count = grid_size[0] * grid_size[1]);
            if (speed <= 0) {
                speed = 1;
            }

            options = Object.extend({
                frames: [],
                looped: true,
                loop_back_to: 0, // restart on the first frame
                on_animation_finish: function(){ }
            }, options || {});

            this.surface = image;

            // build frame matrix
            this.frames = options.frames;
            if (this.frames.length == 0) {
                this._buildFrames($R(start, start + count, true).toArray(),
                                  grid_size, frame_size);
            }

            this.old_st = null;
            this.frame = start;
            this.num_frames = this.frames.length;
            this.speed = (speed || 1) / 1000;
            this.looped = options.looped;
            this.loop_back_to = options.loop_back_to;
            this.finished = false;
            this.onAnimationFinish = options.on_animation_finish;

            // inheirt Surface's methods
            this.blit = image.blit.bind(image);
            this.scroll = image.scroll.bind(image);
            this.getStyle = image.getStyle.bind(image);
            this.setStyle = image.setStyle.bind(image);
            this.rotate = image.rotate.bind(image);
            this.scale = image.scale.bind(image);
            this.setOpacity = image.setOpacity.bind(image);
            this.setSize = image.setSize.bind(image);
        },


        /**
         * lily.graphics.dom.anim.Strip#_buildFrames(frames) -> undefined
         * - frames (Array): array of frame indexes
         * 
         * Build the frame matrix.
         **/
        _buildFrames: function(frames, grid_size, frame_size)
        {
            this.frames = [];
            var l = frames.length;
            while (l--) {
                var i = frames[l];
                var x = (i % grid_size[0]) * frame_size[0];
                var y = ~~(i / grid_size[0]) * frame_size[1];
                this.frames = [[x, y]].concat(this.frames);
            }
            this.num_frames = this.frames.length;
        },


        /**
         * lily.graphics.dom.anim.Strip#update(lag) -> undefined
         * - lag (Number): amount of unexpected delay between frames. If not
         *   given, it'll be extracted from the internal timebase. 
         * 
         * Updates the animation.
         **/
        update: function(lag)
        {
            if (!this.looped && this.finished) {
                return;
            }

            if (!lag) {
                var st = +new Date();
                lag = st - (this.old_st || (this.old_st = st));
            }

            this.frame += this.speed * lag;
            if (this.frame >= this.num_frames - 1) {
                this.onAnimationFinish();
                this.finished = true;

                var delta = this.frame;
                this.frame = this.loop_back_to;
                if (this.looped) {
                    this.frame = (this.frame + delta) % this.num_frames;
                }
            }
            var pos = this.frames[~~this.frame]; /* floor frame */
            this.surface._canvas.style.backgroundPosition = (-pos[0]) + "px " +
                                                            (-pos[1]) + "px";
        }
    });


    /**
     * class lily.graphics.dom.anim.Morph
     * 
     * Implements morph animations. Morph animations works by changing certain
     * properties of a surface over the duration of the animation, directly on
     * the Surface's _change_list for a better performance.
     *
     * The object can either use the internal object timebase or the
     * application time base. The former allows you to update the object only
     * when needed (the object is visible on the screen);
     *
     * Basic surface manipulations are inherited from the DOM Surface object.
     **/
    lily.graphics.dom.anim.Morph = Class.create(
    {
        /**
         * new lily.graphics.dom.anim.Morph(image, from, to, duration[, bounce = false][, repeat = 1][, time_warp][, options])
         * - image (String | [[lily.graphics.Surface]]): image used by this
         *   animation. If a String pointing to an image URL is provided, a
         *   [[lily.graphics.Surface]] will be created and the image will be
         *   loaded in it.
         * - from (Object): An object containing the properties to animate and
         *   their initial values.
         * - to (Object): An object of the final values of the properties to
         *   animate, **must** contain the same keys as `from`.
         * - duration (Number): duration of the animation, in miliseconds.
         * - bounce (Boolean): wether to use a two-way (from - to, then to -
         *   from) animation or not.
         * - repeat (Number): number of times to repeat the animation, or zero
         *   to repeat endlessly.
         * - time_warp (Function): a function that takes the current position
         *   (as a float between 0 and 1) and returns another position that
         *   will be used instead.
         * - options (Object): additional animation options
         * 
         * The following properties can be animated:
         * 
         * - `angle` (`Number`): object's rotation in degrees.
         * - `scale_x` (`Number`): horizontal scale
         * - `scale_y` (`Number`): vertical scale
         * - `opacity` (`Number`): object's opacity 
         **/
        initialize: function(image, from, to, duration, bounce, repeat,
                             time_warp, options)
        {
            if (Object.isString(image)) {
                image = new lily.graphics.dom.Surface(image);
            }

            if (!Object.isNumber(repeat)) {
                repeat = 1;
            }

            options = Object.extend({
                on_animation_finish: function(times_played){ }
            }, options || {});

            this.surface = image;

            this.from = from;
            this.to = to;
            this.old_st = null;
            this.time = 0;
            this.times_played = 0;
            this.reversed = false;
            this.duration = duration * 1000;
            this.repeat = repeat;
            this.bounce = bounce;
            this.time_warp = time_warp;
            this.onAnimationFinish = options.on_animation_finish;

            // inheirt Surface's methods
            this.blit = image.blit.bind(image);
        },


        /**
         * lily.graphics.dom.anim.Morph#update(lag) -> undefined
         * - lag (Number): unexpected delay between the frames. If not given,
         *   this will be extracted from the internal timebase. 
         * 
         * Updates the animation.
         **/
        update: function(lag)
        {
            if (this.times_played && this.times_played >= this.repeat) {
                return;
            }

            if (!lag) {
                var st = +new Date();
                this.old_st || (this.old_st = st);
                lag = st - this.old_st;
            }

            this.time += lag;
            var p = (this.time < this.duration ? this.time : this.duration)
                  / this.duration;

            if (this.time_warp) {
                p = this.time_warp(p);
            }
            if (this.reversed) {
                p = 1 - p;
            }

            // change the Surface's properties.
            var surf = this.surface;
            var change_list = surf._change_list;
            for (var key in this.from) {
                var v = this.from[key] + (this.to[key] - this.from[key]) * p;
                if (key == "angle") {
                    surf.angle = v % 360;
                    change_list['rotation'] = true;
                } else {
                    surf[key] = v;

                    if (key == "scale_x" || key == "scale_y") {
                        key = "scale";
                    }
                    if (key == "width" || key == "height") {
                        key = "size";
                    }

                    change_list[key] = true;
                }
            }

            // animation has played
            if (this.time > this.duration) {
                this.time = this.time % this.duration;

                if (this.bounce && !this.reversed) {
                    this.reversed = true;
                } else {
                    this.reversed = false;
                    this.times_played++;
                    this.onAnimationFinish(this.times_played);
                }
            }
        },


        /**
         * lily.graphics.dom.anim.Morph#isFinished() -> Boolean
         * 
         * Checks wether the animation has finished playing or not.
         **/
        isFinished: function()
        {
            return this.times_played && this.times_played >= this.repeat;
        },

        
        /**
         * lily.graphics.dom.anim.Morph#toString() -> String
         * 
         * returns a representation of the initial properties of this object.
         **/
        toString: function() {
            return $H(this.from).inspect();
        }
    });

    /*--
     * Extends the DOM Surface methods to accept a morph animation.
     */
    if (!lily.graphics.dom.Surface.prototype._blit) {
        lily.graphics.dom.Surface.prototype._blit = lily.graphics.dom
                                                    .Surface.prototype.blit;
    }


    lily.graphics.dom.Surface.addMethods({
        /**
         * lily.graphics.dom.Surface#morph(from, to, duration[, bounce=false][, repeat = 1][, time_warp][, options]) -> lily.graphics.dom.Surface
         * 
         * Animates the Surface's properties over the time. This can be
         * chained. Any subsequent calls to this property will put the given
         * morph animation on the queue.
         *
         * For a list of the parameters, see [[lily.graphics.dom.anim.Morph]]
         **/
        morph: function(from, to, duration, bounce, repeat, time_warp, options)
        {
            options || (options = {});

            this.morph_list || (this.morph_list = []);
            var morph = this._morph;
            var surf = this;

            function runMorph() {
                if (!surf._morph || surf._morph.isFinished()) {
                    surf._morph = surf.morph_list.shift();
                }
                if (surf._morph) {
                    surf._morph.onAnimationFinish = function(times) {
                        if (options.on_animation_finish) {
                            options.on_animation_finish(times);
                        }

                        if (morph.isFinished()) {
                            runMorph();
                        }
                    };
                }
            }

            this.morph_list.push(new lily.graphics.dom.anim.Morph(surf, from,
                                     to, duration, bounce, repeat, time_warp,
                                     options));
            runMorph();

            return this;
        },


        // Overwrite Surface's blit to make sure the morph animation is updated.
        blit: function(surface, position)
        {
            if (surface._morph) {
                surface._morph.update();
            }
            this._blit(surface, position);
        }
    });

}); // onLoad